package ga;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.PriorityQueue;
import sim.Sim;
import xml.XMLGATracker;

public class GAOptimization {
	
	public static final int nVars = 7;
	
	PriorityQueue<WeightScore> population;
	int ngenerations;
	int npopulation;
	
	double best = 0;
	double w[] = new double[nVars];
	int numNotDiff;
	
	public GAOptimization () {
		this(10,10);
		numNotDiff = 0;
	}
	
	public GAOptimization(int ngenerations, int npopulation) {
		this.ngenerations = ngenerations;
		this.npopulation = npopulation;
		population = new PriorityQueue<WeightScore>();
	}
	
	private void constructInitialPopulation() {
		// uses values from -1 to 1 as values for the weights
		population.clear();
		
		for (int i=0; i<npopulation; i++) {
			double w1 = Math.random() * ((Math.random()) < 0.5 ? -1 : 1);
			double w2 = Math.random() * ((Math.random()) < 0.5 ? -1 : 1);
			double w3 = Math.random() * ((Math.random()) < 0.5 ? -1 : 1);
			double w4 = Math.random() * ((Math.random()) < 0.5 ? -1 : 1);
			double w5 = Math.random() * ((Math.random()) < 0.5 ? -1 : 1);
			double w6 = Math.random() * ((Math.random()) < 0.5 ? -1 : 1);
			double w7 = Math.random() * ((Math.random()) < 0.5 ? -1 : 1);
			
			WeightSet ws = new WeightSet(w1,w2,w3,w4,w5,w6,w7);
			
			WeightScore wsc = new WeightScore(runSimulationWithWeights(ws)/*testScoreGenerator(ws)*/,ws);
			
			population.add(wsc);			
		}
	}
	
	private WeightSet getParent() {
		// We will choose from the best five individuals in the population as parents
		
		//	|
		//.6|
		//	|
		//.4|.
		//	|      .
		//.2|             .
		//	|                   .
		//	---------------------------------
		//	0	1	2	3	4	5
		
		// construct a line y = mx+b where the area under the line
		// is 1
		
		// area = 0.5(height * base) = 1
		// we know that the base is 5 to make five intervals
		
		// therefore: height = 2/5
		
		// the slope then is m = (y1-y2)/(x1-x2)
		// we have two points (0,2/5) and (6,0)
		
		// m = -(2/30) = -1/15
		
		// y = (-1/15)x+(2/5)
		
		// the area between [0,1) = area under the curve - area between [1,5];
		
		double y1 = (-1.0/15)*1+(2.0/5);
		double y2 = (-1.0/15)*2+(2.0/5);
		double y3 = (-1.0/15)*3+(2.0/5);
		double y4 = (-1.0/15)*4+(2.0/5);
		
		double i1 = 1-(0.5*y1*4);
		double i2 = 1-(0.5*y2*3);
		double i3 = 1-(0.5*y3*2);
		double i4 = 1-(0.5*y4*1);
		// i5 = 1
		
		// let's choose a random number and see what bucket we're in
		
		double ran = Math.random();
		int bucket = 0;
		
		if (ran < i1) { bucket = 0; }
		else if (ran < i2) { bucket = 1; }
		else if (ran < i3) { bucket = 2; }
		else if (ran < i4) { bucket = 3; }
		else { bucket = 4; }
		
		return ((WeightScore)population.toArray()[bucket]).ws;
	}
	
	private WeightSet getChild() {
		WeightSet p1 = getParent();
		WeightSet p2 = getParent();
		
		while (p1.equals(p2)) {
			p2 = getParent(); // we don't want to breed two of the same individual
		}
		
		double w1 = ((Math.random()<0.5)? p1.w.get(0) : p2.w.get(0));
		double w2 = ((Math.random()<0.5)? p1.w.get(1) : p2.w.get(1));
		double w3 = ((Math.random()<0.5)? p1.w.get(2) : p2.w.get(2));
		double w4 = ((Math.random()<0.5)? p1.w.get(3) : p2.w.get(3));
		double w5 = ((Math.random()<0.5)? p1.w.get(4) : p2.w.get(4));
		double w6 = ((Math.random()<0.5)? p1.w.get(5) : p2.w.get(5));
		double w7 = ((Math.random()<0.5)? p1.w.get(6) : p2.w.get(6));
		
		if (Math.random() < 0.1) {
			// 10% chance to inject some mutation
			int index = (int)(Math.random()*7);
			switch(index) {
			case 0: w1 = Math.random()*((Math.random()<0.5)? 1 : -1); break;
			case 1: w2 = Math.random()*((Math.random()<0.5)? 1 : -1); break;
			case 2: w3 = Math.random()*((Math.random()<0.5)? 1 : -1); break;
			case 3: w4 = Math.random()*((Math.random()<0.5)? 1 : -1); break;
			case 4: w5 = Math.random()*((Math.random()<0.5)? 1 : -1); break;
			case 5: w6 = Math.random()*((Math.random()<0.5)? 1 : -1); break;
			case 6: w7 = Math.random()*((Math.random()<0.5)? 1 : -1); break;
			default: break;
			}
		}
		
		return new WeightSet(w1,w2,w3,w4,w5,w6,w7);		
	}
	
	private void getNextGeneration() {
		PriorityQueue<WeightScore> p = new PriorityQueue<WeightScore>();
		for(int i=0; i<npopulation; i++) {
			WeightSet child = getChild();
			p.add(new WeightScore(runSimulationWithWeights(child)/*testScoreGenerator(child)*/,child));
		}
		
		//population.clear();
		//population = null;
		population.addAll(p);
		
	}
	
	public double[] start() {
		return startAtGeneration(0);
		
	}
	
	public double[] startAtGeneration(int g) {
		for(int i=g; i<=ngenerations; i++) {
			// Initial population
			if (i==0) {
				constructInitialPopulation();
				XMLGATracker.getInstance().addGeneration(0, population);
				XMLGATracker.getInstance().writeXML();
				
				best = ((WeightScore)population.toArray()[0]).score;
				for (int j=0; j<w.length; j++) {
					w[j] = ((WeightScore)population.toArray()[0]).ws.w.get(j);
				}
				/*
				w[0] = ((WeightScore)population.toArray()[0]).ws.w.get(0);
				w[1] = ((WeightScore)population.toArray()[0]).ws.w.get(1);
				w[2] = ((WeightScore)population.toArray()[0]).ws.w.get(2);
				w[3] = ((WeightScore)population.toArray()[0]).ws.w.get(3);
				w[4] = ((WeightScore)population.toArray()[0]).ws.w.get(4);
				w[5] = ((WeightScore)population.toArray()[0]).ws.w.get(5);
				*/
				
			} else {
				getNextGeneration();
				
				XMLGATracker.getInstance().addGeneration(i, population);
				XMLGATracker.getInstance().writeXML();
				
				WeightScore possible_best = ((WeightScore)population.toArray()[0]);
				double possible_best_score = possible_best.score;
				if (possible_best_score<best) {
					double tmp = best;
					best = possible_best_score;
					for (int j=0; j< w.length; j++) {
						w[j] = ((WeightScore)population.toArray()[0]).ws.w.get(j);
					}
					/*
					w[0] = ((WeightScore)population.toArray()[0]).ws.w.get(0);
					w[1] = ((WeightScore)population.toArray()[0]).ws.w.get(1);
					w[2] = ((WeightScore)population.toArray()[0]).ws.w.get(2);
					w[3] = ((WeightScore)population.toArray()[0]).ws.w.get(3);
					w[4] = ((WeightScore)population.toArray()[0]).ws.w.get(4);
					w[5] = ((WeightScore)population.toArray()[0]).ws.w.get(4);
					*/
					if ((tmp-possible_best_score)/((double)tmp)<0.01) { // only 1% change 
						numNotDiff++;
						// Termination Condition.
						if(numNotDiff > 3)
							break;
					}
				}
			}
			
			System.out.println("Generation: "+i+"\tbest: "+best+"["+w[0]+","+w[1]+","+w[2]+","+w[3]+","+w[4]+","+w[5]+","+w[6]+"]");
			
		} 
		
		return constructResultSet();
	}
	
	private double[] constructResultSet() {
		double rv[] = new double[w.length+1];
		for (int i=0; i<rv.length; i++) {
			if (i==0) rv[i] = best;
			else rv[i]=w[i-1];
		}
		return rv;
	}
	
	public double[] restart() {
		double[] bestSoFar = XMLGATracker.getInstance().getBestWeightSet();
		for (int i=0; i<bestSoFar.length; i++) {
			if (i==0) { best = bestSoFar[i]; } 
			else { w[i-1] = bestSoFar[i]; }
			
		}
		int gen = XMLGATracker.getInstance().getOldestGeneration();
		population = XMLGATracker.getInstance().getPopulationAtGeneration(gen);
		
		startAtGeneration(gen);
	
	return constructResultSet();
		
	}
	
	private int runSimulationWithWeights(WeightSet ws) {
		String filename;
		int score=0;
		for(int i=1; i<11; i++) {
			for(int j=0; j<5; j++) {
				filename = "config_"+i+"_"+j;
				int sim_score = 0;
				for(int k=0; k<5; k++) {
					sim_score = Sim.runOnce(filename, ws.w.get(0), ws.w.get(1), ws.w.get(2), ws.w.get(3),ws.w.get(4),ws.w.get(5),ws.w.get(6));
					if (sim_score > -1)
						break;
				}
				
				score += ((sim_score > -1) ? sim_score : 50000);
				
			}
			
		}
		
		return score;
		
	}
	
	private int testScoreGenerator(WeightSet ws) {
		
		return (int)(Math.random()*Integer.MAX_VALUE/5);
	}
	
	
	public int test() {
		// Toar's intuitive weights (distance, angle, obstacle and velocity only)
		// effort				// 0.89
		// priority				//-0.61
		// distance // -1.00	// 0.20
		// angles   // -0.03	// 0.67
		// obstacles// -0.03	// 0.96
		// cost					//-0.18
		// velocity	// +0.25	//-0.02
		
		// "Intuitive Weights"
		//return runSimulationWithWeights(new WeightSet(0,0,-1.0,-0.03,-0.03,0,0.25));
		
		//"Intuitive" weights thought up by myself
		//0.20,-0.50,1.00,0.03,0.03,-0.03,-0.25
		return runSimulationWithWeights(new WeightSet(0.20,-0.50,1.00,0.03,0.03,-0.03,-0.25));
		
	}
	
	public static void main(String args[]) throws Exception {
		boolean test = true;
		// Test the weights produced by the GA
		if (test) {
				GAOptimization ga = new GAOptimization();
				FileOutputStream Output;
			    PrintStream file = null;
			          try
			          {     
			               Output = new FileOutputStream("results.txt",true);
			               file = new PrintStream(Output);
			          }
			          catch(Exception e)
			          {
			               System.out.println("Could not load file");
			          }
			          
			          file.println("Intuitive weights: 0.20,-0.50,1.00,0.03,0.03,-0.03,-0.25");
			          // Run the simulation i times
					   for(int i = 0; i<250; i++){
						   file.println(ga.test());
					   }
			}
		// Get the GA to produce an optimal weight set
		else {
			System.setOut(new PrintStream(new BufferedOutputStream(new FileOutputStream("stdout_log.txt",true)),true));
			System.setErr(new PrintStream(new BufferedOutputStream(new FileOutputStream("stdout_log.txt",true)),true));
	
			
			GAOptimization ga = new GAOptimization();
			double[] weights = null;
			
			try {
				weights = ga.start();
			} catch (Exception e) {
				XMLGATracker.getInstance().writeXML();
				System.out.println(e);
				e.printStackTrace();
			}
			
			System.out.println("score: "+weights[0]+
								"\nw1="+weights[1]+
								"\nw2="+weights[2]+
								"\nw3="+weights[3]+
								"\nw4="+weights[4]+
								"\nw5="+weights[5]+
								"\nw6="+weights[6]+
								"\nw7="+weights[7]);
		
		}
	}
}
